package com.dbflow5.processor;

import com.dbflow5.annotation.*;
import com.dbflow5.contentprovider.annotation.ContentProvider;
import com.dbflow5.contentprovider.annotation.TableEndpoint;
import com.dbflow5.converter.TypeConverters;
import com.dbflow5.processor.definition.*;
import com.dbflow5.processor.definition.provider.ContentProviderDefinition;
import com.dbflow5.processor.definition.provider.TableEndpointDefinition;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;

import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors;

public class Handlers {

    /**
     * Description: The main base-level handler for performing some action when the
     * [DBFlowProcessor.process] is called.
     */
    interface Handler {

        /**
         * Called when the process of the [DBFlowProcessor] is called
         *
         * @param processorManager The manager that holds processing information
         *                         *
         * @param roundEnvironment The round environment
         */
        void handle(ProcessorManager processorManager, RoundEnvironment roundEnvironment);
    }

    /**
     * Description: The base handler than provides common callbacks into processing annotated top-level elements
     */
    abstract static class AnnotatedHandler<AnnotationClass extends Annotation> implements Handler {
        private final Class<AnnotationClass> annotationClass;

        public AnnotatedHandler(Class<AnnotationClass> annotationClass) {
            this.annotationClass = annotationClass;
        }

        @Override
        public void handle(ProcessorManager processorManager, RoundEnvironment roundEnvironment) {
            Set<Element> annotatedElements = (Set<Element>) roundEnvironment.getElementsAnnotatedWith(annotationClass);
            processElements(processorManager, annotatedElements);
            if (annotatedElements.size() > 0) {
                annotatedElements.forEach(element -> {
                    if(element != null) {
                        AnnotationClass annotation = element.getAnnotation(annotationClass);
                        if (annotation != null) {
                            onProcessElement(annotation, element, processorManager);
                        }
                    }
                });
                afterProcessElements(processorManager);
            }
        }

        public void processElements(ProcessorManager processorManager, Set<Element> annotatedElements) {

        }

        protected abstract void onProcessElement(AnnotationClass annotation, Element element, ProcessorManager processorManager);

        public void afterProcessElements(ProcessorManager processorManager) {

        }
    }

    /**
     * Description: Handles [Migration] by creating [MigrationDefinition]
     * and adds them to the [ProcessorManager]
     */
    public static class MigrationHandler extends AnnotatedHandler<Migration> {

        public MigrationHandler() {
            super(Migration.class);
        }

        @Override
        public void onProcessElement(Migration annotation, Element element, ProcessorManager processorManager) {
            if (element instanceof TypeElement) {
                Migration migration = element.getAnnotation(Migration.class);
                if (migration != null) {
                    MigrationDefinition migrationDefinition = new MigrationDefinition(migration, processorManager, (TypeElement) element);
                    processorManager.addMigrationDefinition(migrationDefinition);
                }
            }
        }
    }

    /**
     * Description: Handles [ModelView] annotations, writing
     * ModelViewAdapters, and adding them to the [ProcessorManager]
     */
    public static class ModelViewHandler extends AnnotatedHandler<ModelView> {
        public ModelViewHandler() {
            super(ModelView.class);
        }

        public void onProcessElement(ModelView annotation, Element element, ProcessorManager processorManager) {
            if (element instanceof TypeElement) {
                ModelView modelView = element.getAnnotation(ModelView.class);
                if (modelView != null) {
                    ModelViewDefinition modelViewDefinition = new ModelViewDefinition(modelView, processorManager, (TypeElement) element);
                    processorManager.addModelViewDefinition(modelViewDefinition);
                }
            }
        }
    }

    /**
     * Description: Handles [QueryModel] annotations, writing QueryModelAdapter, and
     * adding them to the [ProcessorManager].
     */
    static class QueryModelHandler extends AnnotatedHandler<QueryModel> {

        public QueryModelHandler() {
            super(QueryModel.class);
        }

        @Override
        public void onProcessElement(QueryModel annotation, Element element, ProcessorManager processorManager) {
            if (element instanceof TypeElement) {
                QueryModel queryModel = element.getAnnotation(QueryModel.class);
                if (queryModel != null) {
                    QueryModelDefinition queryModelDefinition = new QueryModelDefinition(queryModel, (TypeElement) element, processorManager);
                    processorManager.addQueryModelDefinition(queryModelDefinition);
                }
            }
        }
    }

    static class TableEndpointHandler extends AnnotatedHandler<TableEndpoint> {

        private Validators.TableEndpointValidator validator = new Validators.TableEndpointValidator();

        public TableEndpointHandler() {
            super(TableEndpoint.class);
        }

        @Override
        public void onProcessElement(TableEndpoint annotation, Element element, ProcessorManager processorManager) {
            // top-level only
            if (element.getEnclosingElement() instanceof PackageElement) {
                TableEndpointDefinition tableEndpointDefinition = new TableEndpointDefinition(annotation, element, processorManager);
                if (validator.validate(processorManager, tableEndpointDefinition)) {
                    processorManager.putTableEndpointForProvider(tableEndpointDefinition);
                }
            }
        }
    }

    /**
     * Description: Handles [Table] annotations, writing ModelAdapters,
     * and adding them to the [ProcessorManager]
     */
    static class TableHandler extends AnnotatedHandler<Table> {

        public TableHandler() {
            super(Table.class);
        }

        @Override
        public void onProcessElement(Table annotation, Element element, ProcessorManager processorManager) {
            if (element instanceof TypeElement) {
                TableDefinition tableDefinition = new TableDefinition(annotation, processorManager, (TypeElement) element);
                processorManager.addTableDefinition(tableDefinition);

                ManyToMany manyToMany = element.getAnnotation(ManyToMany.class);
                if (manyToMany != null) {
                    ManyToManyDefinition manyToManyDefinition = new ManyToManyDefinition((TypeElement) element, processorManager, manyToMany);
                    processorManager.addManyToManyDefinition(manyToManyDefinition);
                }

                if (element.getAnnotation(MultipleManyToMany.class) != null) {
                    MultipleManyToMany multipleManyToMany = element.getAnnotation(MultipleManyToMany.class);
                    if (multipleManyToMany != null) {
                        for (ManyToMany many : multipleManyToMany.value()) {
                            processorManager.addManyToManyDefinition(new ManyToManyDefinition((TypeElement) element, processorManager, many));
                        }
                    }
                }
            }
        }
    }

    /**
     * Description: Handles [TypeConverter] annotations,
     * adding default methods and adding them to the [ProcessorManager]
     */
    static class TypeConverterHandler extends AnnotatedHandler<TypeConverter> {

        private Set<Element> typeConverterElements = new HashSet<>();
        private Set<TypeConverterDefinition> typeConverterDefinitions = new HashSet<>();

        public TypeConverterHandler() {
            super(TypeConverter.class);
        }

        @Override
        public void processElements(ProcessorManager processorManager, Set<Element> annotatedElements) {
            for (Class<?> clazz : DEFAULT_TYPE_CONVERTERS) {
                typeConverterElements.add(processorManager.elements.getTypeElement(clazz.getName()));
            }
            annotatedElements.addAll(typeConverterElements);
        }

        public void onProcessElement(TypeConverter annotation, Element element, ProcessorManager processorManager) {
            if (element instanceof TypeElement) {
                ClassName className = ProcessorUtils.fromTypeMirror(element.asType(), processorManager);
                if (className != null) {
                    TypeConverterDefinition definition = new TypeConverterDefinition(annotation, className, element.asType(), processorManager, typeConverterElements.contains(element));
                    if (VALIDATOR.validate(processorManager, definition)) {
                        // allow user overrides from default.
                        // Check here if user already placed definition of same type, since default converters
                        // are added last.
                        LinkedHashMap<TypeName, TypeConverterDefinition> converts = new LinkedHashMap<>();
                        for (Map.Entry<TypeName, TypeConverterDefinition> entry : processorManager.typeConverters.entrySet()) {
                            if (entry.getValue().modelTypeName == definition.modelTypeName) {
                                converts.put(entry.getKey(), entry.getValue());
                            }
                        }
                        if (converts.isEmpty()) {
                            typeConverterDefinitions.add(definition);
                        }
                    }
                }
            }
        }

        public void afterProcessElements(ProcessorManager processorManager) {
            // validate multiple global registered do not exist.
            Map<TypeName, List<TypeConverterDefinition>> grouping = typeConverterDefinitions.stream().filter(typeConverterDefinition -> typeConverterDefinition.isDefaultConverter)
                    .collect(Collectors.groupingBy(typeConverterDefinition -> typeConverterDefinition.modelTypeName));

            // sort default converters first so that they can get overwritten
            typeConverterDefinitions
                    .stream().sorted(Comparator.comparing(typeConverterDefinition -> !typeConverterDefinition.isDefaultConverter))
                    .forEach(processorManager::addTypeConverterDefinition);
        }

        private static final Validators.TypeConverterValidator VALIDATOR = new Validators.TypeConverterValidator();
        private static final Class<?>[] DEFAULT_TYPE_CONVERTERS = new Class[]{TypeConverters.CalendarConverter.class, TypeConverters.BigDecimalConverter.class, TypeConverters.BigIntegerConverter.class,
                TypeConverters.DateConverter.class, TypeConverters.SqlDateConverter.class, TypeConverters.BooleanConverter.class, TypeConverters.UUIDConverter.class,
                TypeConverters.CharConverter.class
        };
    }

    static class ContentProviderHandler extends AnnotatedHandler<ContentProvider> {

        public ContentProviderHandler() {
            super(ContentProvider.class);
        }

        @Override
        public void onProcessElement(ContentProvider annotation, Element element, ProcessorManager processorManager) {
            ContentProviderDefinition contentProviderDefinition = new ContentProviderDefinition(annotation, element, processorManager);
            if (contentProviderDefinition.elementClassName != null) {
                processorManager.addContentProviderDefinition(contentProviderDefinition);
            }
        }
    }

    /**
     * Description: Deals with writing database definitions
     */
    public static class DatabaseHandler extends AnnotatedHandler<Database> {

        private final Validators.DatabaseValidator validator = new Validators.DatabaseValidator();

        public DatabaseHandler() {
            super(Database.class);
        }

        public void onProcessElement(Database annotation, Element element, ProcessorManager processorManager) {
            DatabaseDefinition managerWriter = new DatabaseDefinition(annotation, processorManager, element);
            if (validator.validate(processorManager, managerWriter)) {
                processorManager.addDatabaseDefinition(managerWriter);
            }
        }

        public static final String TYPE_CONVERTER_MAP_FIELD_NAME = "typeConverters";
        public static final String MODEL_ADAPTER_MAP_FIELD_NAME = "modelAdapters";
        public static final String QUERY_MODEL_ADAPTER_MAP_FIELD_NAME = "queryModelAdapterMap";
        public static final String MIGRATION_FIELD_NAME = "migrationMap";
        public static final String MODEL_VIEW_ADAPTER_MAP_FIELD_NAME = "modelViewAdapterMap";
        public static final String MODEL_NAME_MAP = "modelTableNames";
    }
}